//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  ExchangeUserProxy.cs
//    
// Abstract:
//
//  This module implements the ExchangeUserProxy class. This class encapsulates an Enhanced Reminders user. The user is 
//  identified by an e-mail address. Information is obtained from Exchange via EWS using either impersonation or delegation. 
//  This is done via a WCF proxy. All EWS requests are made using asynchronous methods for the sake of scalability.
//
//  NOTE: Currently there is no enforced limit on the number of reminders a user can schedule at any one time. 
//  However, in the interest of preventing certain potential security issues an enterprise targeted application 
//  should enforce a limit on the number of reminders that a user can have in the schedule.
//
//--

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security;

using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler
{
    /// <summary>
    /// Encapsulates an Enhanced Reminders user.
    /// </summary>
    internal sealed class ExchangeUserProxy : IDisposable
    {
        // Indicates whether delegation or impersonation is to be used for accessing mailboxes
        private readonly static bool _useDelegation = Helpers.GetSetting<bool>(Constants.UseDelegationSetting, false);

        // WCF proxy for communicating with EWS
        private ExchangeServicePortType _client;

        // Impersonation details for this user
        private readonly ExchangeImpersonationType _impersonation;

        // Mailbox address for this user
        private readonly string _emailAddress;

        // Display name for this user
        private readonly string _displayName;

        // Table of monitors
        private readonly Dictionary<string, Monitor> _monitorTable;

        // Synchronization object. This is used to synchronize access to the monitor table.
        private readonly object _syncRoot = new object();

        // Serializes handling of notification updates and other events
        private readonly TaskSerializer _taskSerializer;

        // This user's Enhanced Reminders rules. These govern sending notifictaions on incoming mailbox items and applying 
        // rules to calendar items.
        private NotificationRules _notificationRules;

        // Master channel list. Used for resolving custom channel references on notifications.
        private NotificationChannelList _masterChannelList;

        // Mailbox item ID of the storage item from which the current rules were extracted
        private ItemIdType _storageItemId;

        // Monitors for the different types of mailbox item of interest
        private ReminderItemMonitor _reminderItemMonitor;
        private StorageItemMonitor _storageItemMonitor;
        private VoicemailRulesItemMonitor _voicemailRulesItemMonitor;
        private CalendarRulesItemMonitor _calendarRulesItemMonitor;

        /// <summary>
        /// ctor.
        /// </summary>
        /// <param name="emailAddress">Mailbox address of user</param>
        /// <param name="displayName">Display name of user</param>
        /// <param name="principalUserName">Principal user name of the user</param>
        public ExchangeUserProxy(string emailAddress, string displayName, string principalUserName)
        {
            Debug.Assert(!String.IsNullOrEmpty(emailAddress), "!String.IsNullOrEmpty(emailAddress)");
            Debug.Assert(!String.IsNullOrEmpty(displayName), "!String.IsNullOrEmpty(displayName)"); 

            _emailAddress = emailAddress;
            _displayName = displayName;

            if (_useDelegation)
            {
                // Rely on delegation to access the specified mailbox
                _impersonation = null;
            }
            else
            {
                // Use impersonation to access the specified mailbox
                _impersonation = new ExchangeImpersonationType();
                _impersonation.ConnectingSID = new ConnectingSIDType();
                _impersonation.ConnectingSID.PrincipalName = principalUserName;
            }

            _taskSerializer = new TaskSerializer();
            _taskSerializer.Failed += TaskSerializer_Failed;

            _monitorTable = new Dictionary<string, Monitor>();
        }

        /// <summary>
        /// Initiates initialization of this object.
        /// </summary>
        /// <remarks>
        /// This method ensures that initialization is handled on the task serializer thread.
        /// </remarks>
        public void Initialize()
        {
            // Create a new logging context for initialization
            Context context = new ServerContext(_emailAddress);

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.Initialize");

            ExecuteDelegateTask initializeTask = new ExecuteDelegateTask(new TaskContext(context), Initialize, null);
            _taskSerializer.Execute(initializeTask, true);
        }

        /// <summary>
        /// Initiates shutdown of this object.
        /// </summary>
        /// <remarks>
        /// This method ensures that shutdown is handled on the task serializer thread.
        /// </remarks>
        public void Shutdown()
        {
            // Create a new logging context for shutdown
            Context context = new ServerContext(_emailAddress);

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.Shutdown");

            ExecuteDelegateTask shutdownTask = new ExecuteDelegateTask(new TaskContext(context), Shutdown, null);
            _taskSerializer.Execute(shutdownTask, true);
        }

        /// <summary>
        /// Handles new items of interest. This could be a reminder or the storage item or something matching a rule.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="change">Description of changed item</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">Subscription ID associated with this notification</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        public bool OnItemCreated(ServerContext context, BaseObjectChangedEventType change, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(context.EmailAddress == _emailAddress, "context.EmailAddress == _emailAddress");
            Debug.Assert(change != null, "change != null");
            Debug.Assert(!String.IsNullOrEmpty(subscriptionId), "!String.IsNullOrEmpty(subscriptionId)");

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.OnItemCreated");

            Monitor monitor = null;

            lock (_syncRoot)
            {
                // Find the monitor associated with the specified subscription ID
                _monitorTable.TryGetValue(subscriptionId, out monitor);
            }

            if (monitor != null)
            {
                return monitor.OnItemCreated(context, change, previousWatermark, subscriptionId);
            }

            // The subscription ID is not recognized, so unsubscribe from further notifications
            return false;
        }

        /// <summary>
        /// Handles modifications to items of interest. This could be a reminder or the storage item or something matching a rule.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="change">Description of changed item</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">Subscription ID associated with this notification</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        public bool OnItemModified(ServerContext context, BaseObjectChangedEventType change, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(context.EmailAddress == _emailAddress, "context.EmailAddress == _emailAddress");
            Debug.Assert(change != null, "change != null");
            Debug.Assert(!String.IsNullOrEmpty(subscriptionId), "!String.IsNullOrEmpty(subscriptionId)");

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.OnItemModified");

            Monitor monitor = null;

            lock (_syncRoot)
            {
                // Find the monitor associated with the specified subscription ID
                _monitorTable.TryGetValue(subscriptionId, out monitor);
            }

            if (monitor != null)
            {
                return monitor.OnItemModified(context, change, previousWatermark, subscriptionId);
            }

            // The subscription ID is not recognized, so unsubscribe from further notifications
            return false;
        }

        /// <summary>
        /// Handles deletion of items of interest. This could be a reminder or the storage item or something matching a rule.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="change">Description of changed item</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">Subscription ID associated with this notification</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        public bool OnItemDeleted(ServerContext context, BaseObjectChangedEventType change, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(context.EmailAddress == _emailAddress, "context.EmailAddress == _emailAddress");
            Debug.Assert(change != null, "change != null");
            Debug.Assert(!String.IsNullOrEmpty(subscriptionId), "!String.IsNullOrEmpty(subscriptionId)");

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.OnItemDeleted");

            Monitor monitor = null;

            lock (_syncRoot)
            {
                // Find the monitor associated with the specified subscription ID
                _monitorTable.TryGetValue(subscriptionId, out monitor);
            }

            if (monitor != null)
            {
                return monitor.OnItemDeleted(context, change, previousWatermark, subscriptionId);
            }

            // The subscription ID is not recognized, so unsubscribe from further notifications
            return false;
        }

        /// <summary>
        /// Handle status updates for the subscriptions associated with this mailbox.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="watermark">Watermark for this notification</param>
        /// <param name="previousWatermark">Previous watermark for this notification</param>
        /// <param name="subscriptionId">Subscription ID associated with this update notification</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        public bool OnStatusUpdated(ServerContext context, string watermark, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(context.EmailAddress == _emailAddress, "context.EmailAddress == _emailAddress");
            Debug.Assert(!String.IsNullOrEmpty(subscriptionId), "!String.IsNullOrEmpty(subscriptionId)");

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.OnStatusUpdated");

            Monitor monitor = null;

            lock (_syncRoot)
            {
                // Find the monitor associated with the specified subscription ID
                _monitorTable.TryGetValue(subscriptionId, out monitor);
            }

            if (monitor != null)
            {
                return monitor.OnStatusUpdated(context, watermark, previousWatermark, subscriptionId);
            }

            // The subscription ID is not recognized, so unsubscribe from further notifications
            return false;
        }

        /// <summary>
        /// Resolves the custom channels on the specified notification using the master channel list for this user.
        /// </summary>
        /// <param name="notificationItem">Notification</param>
        public void ResolveCustomChannels(NotificationItem notificationItem)
        {
            Debug.Assert(notificationItem != null, "notificationItem != null");
            Debug.Assert(notificationItem.EmailAddress == _emailAddress, "notificationItem.EmailAddress == _emailAddress");

            Logger.WriteTraceVerbose(notificationItem.Context, "ExchangeUserProxy.ResolveCustomChannels");

            // Take a local reference because _masterChannelList can be changed on another thread
            NotificationChannelList localMasterChannelList = _masterChannelList;
            notificationItem.ResolveCustomChannels(localMasterChannelList);
        }

        /// <summary>
        /// Closes the Exchange Web Services WCF client and dispose of the monitors.
        /// </summary>
        /// <remarks>
        /// This method should always be called on the task serializer thread.
        /// </remarks>
        public void Dispose()
        {
            if (_client != null)
            {
                Helpers.AssertCast<ExchangeServicePortTypeClient>(_client).Close();
            }

            if (_reminderItemMonitor != null)
            {
                _reminderItemMonitor.Dispose();
                _reminderItemMonitor = null;
            }

            if (_storageItemMonitor != null)
            {
                _storageItemMonitor.Dispose();
                _storageItemMonitor = null;
            }

            if (_voicemailRulesItemMonitor != null)
            {
                _voicemailRulesItemMonitor.Dispose();
                _voicemailRulesItemMonitor = null;
            }

            if (_calendarRulesItemMonitor != null)
            {
                _calendarRulesItemMonitor.Dispose();
                _calendarRulesItemMonitor = null;
            }

            if (_taskSerializer != null)
            {
                // Ensure that no more tasks are executed. This field is not set to null after being disposed because it is 
                // accessed (harmlessly) on non-task-serialized threads. If the field were set to null these threads would
                // need to be synchronized.
                _taskSerializer.Dispose();
            }
        }

        /// <summary>
        /// Returns this user's e-mail address.
        /// </summary>
        public string EmailAddress
        {
            get
            {
                return _emailAddress;
            }
        }

        /// <summary>
        /// Raised when an enhanced reminder for this user is deleted.
        /// </summary>
        public event EventHandler<ItemDeletedEventArgs> ItemDeleted;

        /// <summary>
        /// Raised when an enhanced reminder for this user is either added or modified.
        /// </summary>
        public event EventHandler<ItemUpdatedEventArgs> ItemUpdated;

        /// <summary>
        /// Raised when initialization completes for this user.
        /// </summary>
        public event EventHandler<EventArgs> InitializeCompleted;

        /// <summary>
        /// Raised when an irrecoverable error occurs or when shut down of this user completes during service shut down.
        /// </summary>
        public event EventHandler<EventArgs> Closed;

        /// <summary>
        /// Initiates initialization of this object.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="state">Not used</param>
        /// <remarks>
        /// This method is executed on the task serializer thread.
        /// </remarks>
        private void Initialize(TaskContext taskContext, object state)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");

            // The EWS client proxy should not already be initialized
            Debug.Assert(_client == null, "_client == null");

            Logger.WriteTraceInformation(taskContext.Context, "Searching for CAS server.");

            // Find the CAS server for this user
            string availabilityService = GetAvailabilityService(taskContext.Context);

            if (String.IsNullOrEmpty(availabilityService))
            {
                // Failed to find the CAS server
                OperationalLogs.LogFindingCasError(_emailAddress);
                RaiseClosed();
                return;
            }

            Logger.WriteTraceInformation(taskContext.Context, "Found CAS: {0}.", availabilityService);

            // Found the CAS server, so now create the EWS proxy
            _client = new ExchangeServicePortTypeClient("ExchangeServiceBinding_ExchangeServicePortType", availabilityService);

            // Find the folder IDs for the distinguished folders of interest
            InitializeDistinguishedFolderIds(taskContext);

            // Initialize the monitors
            InitializeMonitors(taskContext);
        }

        /// <summary>
        /// Initiates shutdown of this object.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="state">Not used</param>
        /// <remarks>
        /// This method is executed on the task serializer thread.
        /// </remarks>
        private void Shutdown(TaskContext taskContext, object state)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");

            Logger.WriteTraceVerbose(taskContext.Context, "ExchangeUserProxy.Shutdown");

            lock (_syncRoot)
            {
                if (_monitorTable.Count == 0)
                {
                    // Nothing to shutdown
                    RaiseClosed();
                    return;
                }
            }

            // The monitors should already be initialized
            Debug.Assert(_reminderItemMonitor != null, "_reminderItemMonitor != null");
            Debug.Assert(_storageItemMonitor != null, "_storageItemMonitor != null");
            Debug.Assert(_voicemailRulesItemMonitor != null, "_voicemailRulesItemMonitor != null");
            Debug.Assert(_calendarRulesItemMonitor != null, "_calendarRulesItemMonitor != null");

            // Initiate shutdown of each monitor
            _storageItemMonitor.Shutdown(taskContext);
            _reminderItemMonitor.Shutdown(taskContext);
            _voicemailRulesItemMonitor.Shutdown(taskContext);
            _calendarRulesItemMonitor.Shutdown(taskContext);
        }

        /// <summary>
        /// Get the availability service (CAS) for this user.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <remarks>
        /// This method uses Exchange auto-discovery to find the CAS for this user.
        /// </remarks>
        /// <returns>
        /// The address of the CAS for this user, or null if the server cannot be found.
        /// </returns>
        private string GetAvailabilityService(Context context)
        {
            Debug.Assert(context != null, "context != null");

            Logger.WriteTraceVerbose(context, "ExchangeUserProxy.GetAvailabilityService");

            string availabilityService = null;

            // Identifies the maximum number of redirections through either SCP pointer or Autodiscover redirects
            int maxHops = 10;

            try
            {
                // Call the auto-discover service
                AutoDiscoverResponseXml response = AutoDiscover.Discover(context, _emailAddress, null /* NetworkCredential */, ref maxHops, false);

                if (response != null && response.OutlookData != null && response.OutlookData.Account != null &&
                    response.OutlookData.Account.Protocol != null && response.OutlookData.Account.Protocol.Length > 0)
                {
                    // Use the first address returned by auto-discovery
                    availabilityService = response.OutlookData.Account.Protocol[0].ASUrl;
                }
            }
            catch (ArgumentOutOfRangeException)
            {
                // Ignore max hops being exceeded. If no CAS can be found, this user will be ignored.
            }

            return availabilityService;
        }

        /// <summary>
        /// Finds the distinguished folder IDs for the distinguished folders of interest.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        private void InitializeDistinguishedFolderIds(TaskContext taskContext)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");

            Logger.WriteTraceVerbose(taskContext.Context, "ExchangeUserProxy.InitializeDistinguishedFolderIds");

            DistinguishedFolderIdNameType[] distinguishedFolderIdNames = new DistinguishedFolderIdNameType[] { 
                DistinguishedFolderIdNameType.calendar, DistinguishedFolderIdNameType.deleteditems, DistinguishedFolderIdNameType.junkemail };

            GetDistinguishedFolderTask getDistinguishedFolderTask = new GetDistinguishedFolderTask(
                taskContext, _client, _impersonation, _emailAddress, distinguishedFolderIdNames);
            getDistinguishedFolderTask.Completed += GetDistinguishedFolderTask_Completed;
            _taskSerializer.Execute(getDistinguishedFolderTask);
        }

        /// <summary>
        /// Handles completion of the task to get the distnguished folder IDs.
        /// </summary>
        /// <param name="sender">Get distinguished folder task</param>
        /// <param name="e">Get distinguished folder task completed event args</param>
        private void GetDistinguishedFolderTask_Completed(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.GetDistinguishedFolderTask_Completed");

            sender.AssertCast<GetDistinguishedFolderTask>().Completed -= GetDistinguishedFolderTask_Completed;

            if (e.Error != null)
            {
                // Cannot proceed without the distinguished folder IDs
                RaiseClosed();
                return;
            }

            GetDistinguishedFolderTaskCompletedEventArgs distinguishedFolderEventArgs = e.AssertCast<GetDistinguishedFolderTaskCompletedEventArgs>();
            Debug.Assert(distinguishedFolderEventArgs.FolderIdTable != null, "distinguishedFolderEventArgs.FolderIdTable != null");
        }

        /// <summary>
        /// Initializes all the monitors.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        private void InitializeMonitors(TaskContext taskContext)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");

            // The monitors should not already be initialized
            Debug.Assert(_reminderItemMonitor == null, "_reminderItemMonitor == null");
            Debug.Assert(_storageItemMonitor == null, "_storageItemMonitor == null");
            Debug.Assert(_voicemailRulesItemMonitor == null, "_voicemailRulesItemMonitor == null");
            Debug.Assert(_calendarRulesItemMonitor == null, "_calendarRulesItemMonitor == null");

            Logger.WriteTraceVerbose(taskContext.Context, "ExchangeUserProxy.InitializeMonitors");

            _reminderItemMonitor = new ReminderItemMonitor(_client, _impersonation, _emailAddress, _taskSerializer);
            _reminderItemMonitor.UpdateCompleted += Monitor_UpdateCompleted;
            _reminderItemMonitor.ShutdownCompleted += Monitor_ShutdownCompleted;
            _reminderItemMonitor.ItemDeleted += Monitor_ReminderItemDeleted;
            _reminderItemMonitor.ItemRetrieved += Monitor_ReminderItemRetrieved;

            _storageItemMonitor = new StorageItemMonitor(_client, _impersonation, _emailAddress, _taskSerializer);
            _storageItemMonitor.UpdateCompleted += Monitor_UpdateCompleted;
            _storageItemMonitor.ShutdownCompleted += Monitor_ShutdownCompleted;
            _storageItemMonitor.ItemDeleted += Monitor_StorageItemDeleted;
            _storageItemMonitor.ItemRetrieved += Monitor_StorageItemRetrieved;

            _voicemailRulesItemMonitor = new VoicemailRulesItemMonitor(_client, _impersonation, _emailAddress, _taskSerializer);
            _voicemailRulesItemMonitor.UpdateCompleted += Monitor_UpdateCompleted;
            _voicemailRulesItemMonitor.ShutdownCompleted += Monitor_ShutdownCompleted;
            _voicemailRulesItemMonitor.ItemRetrieved += Monitor_VoicemailRulesItemRetrieved;

            _calendarRulesItemMonitor = new CalendarRulesItemMonitor(_client, _impersonation, _emailAddress, _taskSerializer);
            _calendarRulesItemMonitor.UpdateCompleted += Monitor_UpdateCompleted;
            _calendarRulesItemMonitor.ShutdownCompleted += Monitor_ShutdownCompleted;
            _calendarRulesItemMonitor.ItemRetrieved += Monitor_CalendarRulesItemRetrieved;

            // Always start with the storage item because this is needed in order to handle any other item type
            UpdateMonitor(taskContext, _storageItemMonitor, ExchangeHelpers.GetStorageItemExpression(), false);
            UpdateMonitor(taskContext, _reminderItemMonitor, ExchangeHelpers.GetFutureAndRecurringRemindersExpression(), false);
        }

        /// <summary>
        /// Updates the specified monitor with the specified search expression.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="monitor">Monitor to be updated</param>
        /// <param name="searchExpression">New search expression</param>
        /// <param name="doNext">Indicates if the update should be the next task to be executed, or not</param>
        /// <remarks>
        /// The "doNext" parameter should be true in the case of rules. This is to ensure that the monitor is updated
        /// before any other pending tasks are processed. The "doNext" parameter should be false in the case of reminders
        /// and the stoarge item to ensure that these two monitors are updated in the requested order.
        /// </remarks>
        private void UpdateMonitor(TaskContext taskContext, Monitor monitor, SearchExpressionType searchExpression, bool doNext)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(monitor != null, "monitor != null");

            Guid subscriptionId = Guid.NewGuid();

            lock (_syncRoot)
            {
                _monitorTable.Remove(monitor.SubscriptionId.ToString());
                _monitorTable.Add(subscriptionId.ToString(), monitor);
            }

            monitor.Update(taskContext, searchExpression, subscriptionId, doNext);
        }

        /// <summary>
        /// Handles task serializer failures.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Specifies the logging context</param>
        /// <remarks>
        /// The Failed event should normally only be raised on the serialized task thread. In this case it is being raised on a 
        /// timer callback thread. This is unavoidable in this case because the task serializer is blocked. The fact that the
        /// task serializer thread is blocked should make this safe.
        /// This method raises the closed event directly without attempting to clean-up the search folders. This is because the
        /// clean-up relies on the task serializer thread. The folders will get cleaned up the next time the service starts.
        /// </remarks>
        private void TaskSerializer_Failed(object sender, SchedulerEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");
            
            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.TaskSerializer_Failed");

            // Treat all task serializer errors as unrecoverable for this user
            RaiseClosed();
        }

        /// <summary>
        /// Handles completion of monitor updates.
        /// </summary>
        /// <param name="sender">Monitor that has been updated</param>
        /// <param name="e">Specifies the logging context</param>
        private void Monitor_UpdateCompleted(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_UpdateCompleted");

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }

            if (sender is ReminderItemMonitor)
            {
                // Initialization can be considered completed when the reminder monitor has been updated. This should only happen once.
                RaiseInitializeCompleted();
            }
        }

        /// <summary>
        /// Handles completion of monitor shutdown.
        /// </summary>
        /// <param name="sender">Monitor that has been shutdown</param>
        /// <param name="e">Specifies the logging context</param>
        private void Monitor_ShutdownCompleted(object sender, SchedulerEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_ShutdownCompleted");

            bool shutdownComplete = false;

            lock (_syncRoot)
            {
                Debug.Assert(_monitorTable.Count > 0, "_monitorTable.Count > 0");
                _monitorTable.Remove(sender.AssertCast<Monitor>().SubscriptionId.ToString());
                shutdownComplete = _monitorTable.Count == 0;
            }

            if (shutdownComplete)
            {
                // Once all the monitors have shut down, shut down of this user is complete
                RaiseClosed();
            }
        }

        /// <summary>
        /// Handles deletion of reminders.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Indicates which item has been deleted</param>
        private void Monitor_ReminderItemDeleted(object sender, MonitorItemDeletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");
            Debug.Assert(e.ItemId != null, "e.ItemId != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_ReminderItemDeleted");

            RaiseItemDeleted(e.Context, e.ItemId.Id);
        }

        /// <summary>
        /// Handles deletion of the storage item.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Indicates which item has been deleted</param>
        private void Monitor_StorageItemDeleted(object sender, MonitorItemDeletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.TaskContext != null, "e.TaskContext != null");
            Debug.Assert(e.Context != null, "e.Context != null");
            Debug.Assert(e.ItemId != null, "e.ItemId != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_StorageItemDeleted");

            // Ignore items that don't match the current storage item ID
            if (_storageItemId.Id == e.ItemId.Id)
            {
                OnNewStorageData(e.TaskContext, null /* Storage item data */);
            }
        }

        /// <summary>
        /// Handles new storage items.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Specifies the new storage item and storage data</param>
        private void Monitor_StorageItemRetrieved(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_StorageItemRetrieved");

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }

            GetItemWithAttachmentTaskCompletedEventArgs getItemEventArgs = e.AssertCast<GetItemWithAttachmentTaskCompletedEventArgs>();
            Debug.Assert(getItemEventArgs.Item != null, "getItemEventArgs.Item != null");
            Debug.Assert(getItemEventArgs.FileAttachment != null, "getItemEventArgs.FileAttachment != null");
            Debug.Assert(getItemEventArgs.FileAttachment.Content != null, "getItemEventArgs.FileAttachment.Content != null");

            _storageItemId = getItemEventArgs.Item.ItemId;

            NotificationStorageData storageData = ExtractStorageData(e.Context, getItemEventArgs.FileAttachment);

            // Write out the user channel and rule data for analysis of usage patterns.
            if (storageData != null && Logger.IsEnabled(LogEntryType.Verbose))
            {
                storageData.LogTraceInfo(e.Context);
            }
            
            OnNewStorageData(e.TaskContext, storageData);
        }

        /// <summary>
        /// Handles new/modified reminders.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Specifies the new/modified calendar item</param>
        private void Monitor_ReminderItemRetrieved(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_ReminderItemRetrieved");

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }

            NotificationItem[] notificationItems = null;

            GetItemTaskCompletedEventArgs getItemEventArgs = e.AssertCast<GetItemTaskCompletedEventArgs>();
            Debug.Assert(getItemEventArgs.Item != null, "getItemEventArgs.Item != null");

            CalendarItemType calendarItem = getItemEventArgs.Item as CalendarItemType;

            if (calendarItem != null)
            {
                // If the calendar item is a recurring master then one of two things must happen:
                //   1. If the master item has the reminder property, we must iterate through each occurrence
                //      to determine when the next reminder should be scheduled.
                //   2. If the master item does not have the reminder property, we should still iterate through
                //      the exceptions in the event that any of them have a reminder associated with them.
                if (calendarItem.CalendarItemType1 == CalendarItemTypeType.RecurringMaster || calendarItem.Recurrence != null)
                {
                    string reminderItem = ExchangeHelpers.GetReminderFromExtendedProperties(calendarItem);

                    if (reminderItem != null)
                    {
                        GetRecurringItemTask newTask = new GetRecurringItemTask(e.TaskContext, _client,
                            _impersonation, _emailAddress, calendarItem.ItemId, reminderItem, 
                            ExchangeHelpers.GetDeliveredTimeFromExtendedProperties(calendarItem));

                        Logger.WriteTraceVerbose(e.TaskContext.Context, "Fetching occurrences for recurring meeting.");
                        newTask.Completed += GetRecurringItemTask_Completed;
                        _taskSerializer.Execute(newTask, true);
                        return;
                    }
                    else if (calendarItem.ModifiedOccurrences != null && calendarItem.ModifiedOccurrences.Length > 0)
                    {
                        ItemIdType[] modifiedItemIds = new ItemIdType[calendarItem.ModifiedOccurrences.Length];

                        for (int index = 0; index < modifiedItemIds.Length; index++)
                        {
                            modifiedItemIds[index] = calendarItem.ModifiedOccurrences[index].ItemId;
                        }

                        GetNextModifiedOccurrenceTask newTask = new GetNextModifiedOccurrenceTask(e.TaskContext,
                            _client, _impersonation, _emailAddress, calendarItem.ItemId, 
                            ExchangeHelpers.GetDeliveredTimeFromExtendedProperties(calendarItem), modifiedItemIds);

                        Logger.WriteTraceVerbose(e.TaskContext.Context, "Fetching modified occurrences for recurring meeting with no reminder.");
                        newTask.Completed += GetRecurringItemTask_Completed;
                        _taskSerializer.Execute(newTask, true);
                        return;
                    }
                    else
                    {
                        // Otherwise give up; this recurring meeting has no reminders associated with it.
                        Logger.WriteTraceVerbose(e.TaskContext.Context, "Recurring master has no reminder or modified occurrences. Ignoring.");
                        return;
                    }
                }
                // If this is a rules-based reminder, make any updates to the reminder that are necessary
                else
                {
                    if (UpdateRulesBasedReminder(e.TaskContext, calendarItem))
                    {
                        // Reminder is being updated. A separate notification from EWS will be generated when the update completes.
                        return;
                    }

                    // Create a new notification based on this calendar item
                    notificationItems = ExchangeHelpers.CreateCalendarNotificationItem(e.Context, _emailAddress, _displayName, calendarItem);
                }
            }
            else
            {
                Logger.WriteTraceWarning(e.Context, "Reminder wasn't associated with a calendar item so ignoring.");
            }

            if (notificationItems != null)
            {
                // Add the reminder to the schedule
                RaiseItemUpdated(e.Context, calendarItem.ItemId.Id, notificationItems);
            }
            else
            {
                // The mailbox item did not meet the criteria for an enhanced reminder so make sure it is deleted from the schedule
                RaiseItemDeleted(e.Context, getItemEventArgs.Item.ItemId.Id);
            }
        }

        /// <summary>
        /// Handles adding a recurring meeting occurrence to the schedule if one was found.
        /// </summary>
        /// <param name="sender">The GetRecurringItem task that completed</param>
        /// <param name="e">Specifies the relevant occurrence and reminder item</param>
        private void GetRecurringItemTask_Completed(object sender, TaskCompletedEventArgs e)
        {
            sender.AssertCast<GetItemTask>().Completed -= GetRecurringItemTask_Completed;

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }

            GetNextRecurringItemTaskCompletedEventArgs eventArgs = e.AssertCast<GetNextRecurringItemTaskCompletedEventArgs>();

            NotificationItem[] notificationItems = ExchangeHelpers.CreateCalendarNotificationItemsFromReminderItem(e.Context, _emailAddress, _displayName,
                eventArgs.Item.AssertCast<CalendarItemType>(), eventArgs.ReminderItem, new NotificationId(eventArgs.MasterItemId.Id, _emailAddress, eventArgs.MasterItemId.ChangeKey));

            if (notificationItems != null)
            {
                RaiseItemUpdated(e.Context, eventArgs.MasterItemId.Id, notificationItems);
            }
            else
            {
                RaiseItemDeleted(e.Context, eventArgs.MasterItemId.Id);
            }
        }

        /// <summary>
        /// Handles new mailbox items that potentially match one of the rules.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Specifies the mailbox item and its attachment</param>
        private void Monitor_VoicemailRulesItemRetrieved(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_VoicemailRulesItemRetrieved");

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }

            if (_notificationRules == null)
            {
                // The rules must have been deleted since this voicemail item was detected
                Logger.WriteTraceInformation(e.Context, "Ignoring voicemail item because there are no rules.");
                return;
            }

            GetItemWithAttachmentTaskCompletedEventArgs getItemEventArgs = e.AssertCast<GetItemWithAttachmentTaskCompletedEventArgs>();
            Debug.Assert(getItemEventArgs.Item != null, "taskCompletedEventArgs.Item != null");
            Debug.Assert(getItemEventArgs.FileAttachment != null, "taskCompletedEventArgs.FileAttachment != null");
            Debug.Assert(getItemEventArgs.FileAttachment.Content != null, "taskCompletedEventArgs.FileAttachment.Content != null");

            NotificationItem notificationItem = ExchangeHelpers.CreateVoicemailNotificationItem(
                e.Context, _emailAddress, _displayName, _notificationRules, getItemEventArgs.Item, getItemEventArgs.FileAttachment);

            if (notificationItem != null)
            {
                Logger.WriteTraceInformation(e.Context, "Creating notification for voicemail message.");
                RaiseItemUpdated(e.Context, notificationItem.Id.Id, new NotificationItem[] { notificationItem });
            }
        }

        /// <summary>
        /// Handles new/modified calendar items that potentially match one of the calendar item rules.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Specifies the calendar item</param>
        private void Monitor_CalendarRulesItemRetrieved(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.Monitor_CalendarRulesItemRetrieved");

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }

            if (_notificationRules == null)
            {
                // The rules must have been deleted since this calendar item was detected
                Logger.WriteTraceInformation(e.Context, "Ignoring calendar item because there are no rules.");
                return;
            }

            GetItemTaskCompletedEventArgs getItemEventArgs = e.AssertCast<GetItemTaskCompletedEventArgs>();
            Debug.Assert(getItemEventArgs.Item != null, "getItemEventArgs.Item != null");

            CalendarItemType calendarItem = getItemEventArgs.Item as CalendarItemType;

            if (calendarItem != null)
            {
                NotificationData notificationData = new NotificationData(null, MailboxType.CalendarItem, calendarItem.Start);

                // Check if the calendar item matches any reminder rules
                NotificationRuleItem reminderRule = _notificationRules.GetFirstMatchingReminderRule(notificationData);

                NotificationRuleItem joinNowRule = null;

                if (ExchangeHelpers.IsLiveMeeting(calendarItem))
                {
                    // If this is a Live Meeting, check if the calendar item matches any JoinNow rules
                    joinNowRule = _notificationRules.GetFirstMatchingJoinNowRule(notificationData);
                }

                if (reminderRule != null || joinNowRule != null)
                {
                    // The calendar item matches at least one rule
                    if (reminderRule == null)
                    {
                        Logger.WriteTraceInformation(e.Context, "Calendar item matches JoinNow rule '{0}' so adding a reminder.", joinNowRule.RuleName);
                    }
                    else if (joinNowRule == null)
                    {
                        Logger.WriteTraceInformation(e.Context, "Calendar item matches reminder rule '{0}' so adding a reminder.", reminderRule.RuleName);
                    }
                    else
                    {
                        Logger.WriteTraceInformation(e.Context, "Calendar item matches reminder rule '{0}' and JoinNow rule '{1}' so adding a reminder.", reminderRule.RuleName, joinNowRule.RuleName);
                    }

                    SetReminderTask setReminderTask = new SetReminderTask(e.TaskContext, _client, _impersonation, _emailAddress, calendarItem.ItemId, 
                        reminderRule, joinNowRule, _notificationRules.TimeZoneInfo);
                    setReminderTask.Completed += Task_Completed;
                    _taskSerializer.Execute(setReminderTask, true);
                }
                else
                {
                    // The calendar item doesn't match any rules so ignoring
                    Logger.WriteTraceInformation(e.Context, "Calendar item doesn't match any rules so ignoring.");
                }
            }
            else
            {
                // Mailbox items matching a calendar item rule should be calendar items
                Logger.WriteTraceWarning(e.Context, "Mailbox item macthing calendar rule is not a calendar item so ignoring.");
            }
        }

        /// <summary>
        /// Handles completion of tasks.
        /// </summary>
        /// <param name="sender">Task</param>
        /// <param name="e">Task completed event args</param>
        /// <remarks>
        /// This is a generic task completed event handle for tasks for which only failures need to be handled.
        /// </remarks>
        private void Task_Completed(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");

            sender.AssertCast<Task>().Completed -= Task_Completed;

            if (e.Error != null)
            {
                HandleTaskErrors(e.TaskContext, e.Error);
                return;
            }
        }

        /// <summary>
        /// Handles the specified new storage data
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="storageData">New storage data</param>
        /// <remarks>
        /// This method is called whenever the storage item changes.
        /// </remarks>
        private void OnNewStorageData(TaskContext taskContext, NotificationStorageData storageData)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");
            Debug.Assert(_voicemailRulesItemMonitor != null, "_voicemailRulesItemMonitor != null");
            Debug.Assert(_calendarRulesItemMonitor != null, "_calendarRulesItemMonitor != null");

            Logger.WriteTraceInformation(taskContext.Context, "Applying new rules.");

            NotificationRules oldRules = _notificationRules;

            if (storageData != null)
            {
                _notificationRules = storageData.Rules;
                _masterChannelList = storageData.MasterChannelList;
            }
            else
            {
                _notificationRules = null;
                _masterChannelList = null;
            }

            if (oldRules != null)
            {
                // If there are existing rules, make sure that any rules-based reminders that are now invalid are deleted. There's 
                // no need to do this check if there aren't already rules specified because if there are no rules, there should be
                // no rules-based reminders. At start-up any pre-existing rules-based reminders that are invalid will be dealt with 
                // when each reminder is fetched.
                DeleteInvalidRemindersTask deleteInvalidRemindersTask = new DeleteInvalidRemindersTask(taskContext, _client, _impersonation, 
                    _emailAddress, _notificationRules);
                deleteInvalidRemindersTask.Completed += Task_Completed;
                _taskSerializer.Execute(deleteInvalidRemindersTask, true);
            }

            // Need to update the rules-based monitors based on the new notification rules
            UpdateMonitor(taskContext, _voicemailRulesItemMonitor, ExchangeHelpers.GetSearchExpressionFromRules(taskContext.Context, _notificationRules, MailboxType.Voicemail), true);
            UpdateMonitor(taskContext, _calendarRulesItemMonitor, ExchangeHelpers.GetSearchExpressionFromRules(taskContext.Context, _notificationRules, MailboxType.CalendarItem), true);
        }

        /// <summary>
        /// Handles errors executing tasks.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="error">Error</param>
        private void HandleTaskErrors(TaskContext taskContext, Exception error)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null"); 
            Debug.Assert(error != null, "error != null");

            // Make sure that the error is not irrecoverable. This should have happened when the error was first caught, but 
            // better safe than sorry.
            Helpers.FilterIrrecoverableExceptions(error);

            if (ShouldIgnoreError(error))
            {
                // Some errors should be ignored. For example, if the item doesn't exist, there's no point trying again to fetch it.
                Logger.WriteTraceWarning(taskContext.Context, "Ignoring error.");
                return;
            }

            ExchangeTaskContext exchangeTaskContext = taskContext as ExchangeTaskContext;

            if (exchangeTaskContext != null)
            {
                // If this is a task resulting from a EWS notification, it should have a watermark
                Debug.Assert(!String.IsNullOrEmpty(exchangeTaskContext.Watermark), "!String.IsNullOrEmpty(exchangeTaskContext.Watermark)");

                // Try to recover from this error by re-subscribing for the notification that led to this failure
                Logger.WriteTraceWarning(taskContext.Context, "Re-subscribing for notifications following an error.");
                ResubscribeMonitor(exchangeTaskContext);
            }
            else
            {
                // If the task context isn't an ExchangeTaskContext, this must be a task originating from 
                // initialization. Initialization errors are not recoverable.
                Logger.WriteTraceWarning(taskContext.Context, "Shutting down following an error.");
                Shutdown(taskContext, null);
            }
        }

        /// <summary>
        /// Forces a monitor to re-subscribe for notifications from the point specified in the given task context.
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <remarks>
        /// This method is used to recover from errors accessing EWS.
        /// </remarks>
        private void ResubscribeMonitor(ExchangeTaskContext taskContext)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");

            Monitor monitor = null;

            Guid subscriptionId = Guid.NewGuid();

            lock (_syncRoot)
            {
                // The monitor corrsponding to this task's subcription ID may no longer exist. For example, an earlier error may 
                // already have triggered a re-subscription for this monitor.
                _monitorTable.TryGetValue(taskContext.SubscriptionId.ToString(), out monitor);

                if (monitor != null)
                {
                    _monitorTable.Remove(taskContext.SubscriptionId.ToString());
                    _monitorTable.Add(subscriptionId.ToString(), monitor);
                }
            }

            if (monitor != null)
            {
                monitor.Resubscribe(taskContext.Context, subscriptionId, taskContext.Watermark);
            }
        }

        /// <summary>
        /// Indicates if the specified error should be ignored.
        /// </summary>
        /// <param name="error">Error</param>
        /// <remarks>
        /// Some errors should be ignored and not trigger a retry attempt. For example, if the item does not exist, there is no 
        /// point trying to fetch it again.
        /// </remarks>
        /// <returns>
        /// true if the error should be ignored; false otherwise.
        /// </returns>
        private static bool ShouldIgnoreError(Exception error)
        {
            Debug.Assert(error != null, "error != null");

            ExchangeTaskFailedException taskError = error as ExchangeTaskFailedException;

            if (taskError != null && taskError.ResponseCode == ResponseCodeType.ErrorCorruptData)
            {
                Logger.WriteTraceWarning(taskError.Context, "Corrupted mail item found in user's mailbox. Reminders for this mail item will not be delievered and the user should attempt to find the corrupted item.");
            }

            return taskError != null && (
                taskError.ResponseCode == ResponseCodeType.ErrorCorruptData || // we should not stop due to corrupt items; other mail items are fine. We log a warning and move on.
                taskError.ResponseCode == ResponseCodeType.ErrorItemNotFound ||
                taskError.ResponseCode == ResponseCodeType.NoError || // we return NoError if there were no matching occurrences in a recurring meeting
                taskError.ResponseCode == ResponseCodeType.ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange); // if we hit the end of a recurrence pattern then there is no problem.
        }

        /// <summary>
        /// Makes any updates to the specified rules-based reminder that are necessary (if this is a rules-based reminder).
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="calendarItem">Calendar item associated with the reminder</param>
        /// <returns>
        /// true if the rules-based reminder needed to be updated; false otherwise
        /// </returns>
        private bool UpdateRulesBasedReminder(TaskContext taskContext, CalendarItemType calendarItem)
        {
            Debug.Assert(taskContext != null, "taskContext != null");
            Debug.Assert(taskContext.Context != null, "taskContext.Context != null");
            Debug.Assert(calendarItem != null, "calendarItem != null");

            // Get the IDs of the notification rules associated with this calendar item
            string reminderRuleId = ExchangeHelpers.GetReminderRuleIdFromExtendedProperties(calendarItem);
            string joinNowRuleId = ExchangeHelpers.GetJoinNowRuleIdFromExtendedProperties(calendarItem);

            if (reminderRuleId == null && joinNowRuleId == null)
            {
                // Not a rules-based reminder, so ignore
                return false;
            }

            NotificationRuleItem matchingReminderRule = null;
            NotificationRuleItem matchingJoinNowRule = null;

            // Find the rules that this calendar item matches
            if (_notificationRules != null)
            {
                if (ExchangeHelpers.IsLiveMeeting(calendarItem))
                {
                    matchingJoinNowRule = _notificationRules.GetFirstMatchingJoinNowRule(
                        new NotificationData(null, MailboxType.CalendarItem, calendarItem.Start));
                }

                matchingReminderRule = _notificationRules.GetFirstMatchingReminderRule(
                    new NotificationData(null, MailboxType.CalendarItem, calendarItem.Start));
            }

            if (matchingReminderRule == null && matchingJoinNowRule == null)
            {
                // The calendar item has a rules-based reminder, but doesn't match any rules. Therefore delete the reminder.
                Logger.WriteTraceInformation(taskContext.Context, "Calendar item doesn't match any rules so deleting rules-based reminder.");
                DeleteRemindersTask deleteRemindersTask = new DeleteRemindersTask(taskContext, _client, _impersonation, _emailAddress, 
                    new ItemIdType[] { calendarItem.ItemId });
                deleteRemindersTask.Completed += Task_Completed;
                _taskSerializer.Execute(deleteRemindersTask, true);
                return true;
            }

            Debug.Assert(_notificationRules != null, "_notificationRules != null");

            if (matchingReminderRule == null && reminderRuleId != null ||
                matchingJoinNowRule == null && joinNowRuleId != null ||
                matchingReminderRule != null && reminderRuleId != matchingReminderRule.Id.ToString("D") ||
                matchingJoinNowRule != null && joinNowRuleId != matchingJoinNowRule.Id.ToString("D"))
            {
                // The calendar item has a rules-based reminder, but now matches a different rule. Therefore need to update reminder.
                Logger.WriteTraceInformation(taskContext.Context, "Calendar item now matches different rules so updating the reminder.");
                SetReminderTask setReminderTask = new SetReminderTask(taskContext, _client, _impersonation, _emailAddress, calendarItem.ItemId, 
                    matchingReminderRule, matchingJoinNowRule, _notificationRules.TimeZoneInfo);
                setReminderTask.Completed += Task_Completed;
                _taskSerializer.Execute(setReminderTask, true);
                return true;
            }

            // The calendar item still matches the same rule(s) that generated the associated reminder and/or JoinNow
            Logger.WriteTraceInformation(taskContext.Context, "Calendar item still matches the same rules.");

            return false;
        }

        /// <summary>
        /// Extracts the storage data from the specified attachment.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="fileAttachment">The attachment containing the storage data</param>
        /// <remarks>
        /// The storage item potentially contains user data so cannot be trusted. Therefore, exceptions de-serializing the 
        /// storage item are caught and logged.
        /// </remarks>
        /// <returns>
        /// The NotificationStorageData if the attachment is valid; null otherwise.
        /// </returns>
        private static NotificationStorageData ExtractStorageData(Context context, FileAttachmentType fileAttachment)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(fileAttachment != null && fileAttachment.Content != null, "fileAttachment != null && fileAttachment.Content != null");

            MemoryStream stream = new MemoryStream(fileAttachment.Content);
            BinaryFormatter binaryFormatter = new BinaryFormatter();

            NotificationStorageData storageData = null;

            try
            {
                // De-serialize the NotificationStorageData object contained in the storage item attachment
                storageData = binaryFormatter.Deserialize(stream) as NotificationStorageData;
            }
            catch (SecurityException ex)
            {
                // The reminder has the wrong format
                Logger.WriteTraceWarning(context, ex, "Failed to de-serialize storage data.");
            }
            catch (SerializationException ex)
            {
                // The reminder has the wrong format
                Logger.WriteTraceWarning(context, ex, "Failed to de-serialize storage data.");
            }
            catch (FileLoadException ex)
            {
                // The version of the common assembly is different from that used to create the storage item
                Logger.WriteTraceWarning(context, ex, "Failed to de-serialize storage data.");
            }

            return storageData;
        }

        /// <summary>
        /// Raises the Closed event for this user.
        /// </summary>
        private void RaiseClosed()
        {
            EventHandler<EventArgs> closed = Closed;

            if (closed != null)
            {
                closed(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Raises the InitializeCompleted event for this user.
        /// </summary>
        private void RaiseInitializeCompleted()
        {
            EventHandler<EventArgs> initializeCompleted = InitializeCompleted;

            if (initializeCompleted != null)
            {
                initializeCompleted(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Raises the ItemUpdated event in response to an update to a reminder.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="id">ID associated with the notification(s)</param>
        /// <param name="notificationItems">The notification(s) to be added/modified</param>
        private void RaiseItemUpdated(Context context, string id, NotificationItem[] notificationItems)
        {
            Debug.Assert(!String.IsNullOrEmpty(id), "!String.IsNullOrEmpty(id)");
            Debug.Assert(notificationItems != null, "notificationItems != null");
            Debug.Assert(notificationItems.Length > 0, "notificationItems.Length > 0");

            Logger.WriteTraceInformation(context, "Reminder added.");

            EventHandler<ItemUpdatedEventArgs> itemUpdated = ItemUpdated;

            if (itemUpdated != null)
            {
                itemUpdated(this, new ItemUpdatedEventArgs(id, notificationItems));
            }
        }

        /// <summary>
        /// Raises the ItemDeleted event for the specified reminder.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="id">The ID of the reminder to be deleted</param>
        private void RaiseItemDeleted(Context context, string id)
        {
            Debug.Assert(!String.IsNullOrEmpty(id), "!String.IsNullOrEmpty(id)");

            Logger.WriteTraceInformation(context, "Reminder deleted.");

            EventHandler<ItemDeletedEventArgs> itemDeleted = ItemDeleted;

            if (itemDeleted != null)
            {
                itemDeleted(this, new ItemDeletedEventArgs(id));
            }
        }

        /// <summary>
        /// Marks the time a calendar item had a notification delivered.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="notification">The notification that was delivered</param>
        public void MarkItemAsSent(Context context, NotificationItem notification)
        {
            Logger.WriteTraceVerbose(context, "Marking item as sent.");

            ItemIdType id = new ItemIdType();
            id.Id = notification.Id.Id;
            id.ChangeKey = notification.Id.ChangeKey;

            SetReminderDeliveredTask task = new SetReminderDeliveredTask(new TaskContext(context), _client, _impersonation,
                _emailAddress, id, DateTime.UtcNow);

            task.Completed += SetReminderDelivered_Completed;
            _taskSerializer.Execute(task);
        }

        /// <summary>
        /// Called when a reminder has been flagged as delivered. This currently does nothing except
        /// log a warning if an error occured.
        /// </summary>
        /// <param name="sender">The task that completed</param>
        /// <param name="e">Specifies the modified calendar item</param>
        private void SetReminderDelivered_Completed(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");

            Logger.WriteTraceVerbose(e.Context, "ExchangeUserProxy.SetReminderDelivered_Completed");

            sender.AssertCast<SetReminderDeliveredTask>().Completed -= SetReminderDelivered_Completed;

            if (e.Error != null)
            {
                // Make sure that the error is not irrecoverable. This should have happened when the error was first caught, but 
                // better safe than sorry.
                Helpers.FilterIrrecoverableExceptions(e.Error);

                if (ShouldIgnoreError(e.Error))
                {
                    // Some errors should be ignored. For example, if the item doesn't exist, there's no point trying again to fetch it.
                    Logger.WriteTraceWarning(e.Context, "Ignoring error encountered when trying to mark delivery of notification.");
                }
                else
                {
                    // If this is not an error that we normally ignore, it's still okay for us to ignore this for now;
                    // marking the calendar item as delivered is helpful but not required. If we fail to mark it the
                    // notification should still be delivered.
                    Logger.WriteTraceWarning(e.Context, "Error encountered when trying to mark the delivery time of a notification. {0}", e.Error);
                }
            }
        }
    }
}
